﻿using System;
using System.CodeDom.Compiler;
using log4net;

namespace CSharpExecutor
{
	public class Executor
	{
		#region Constants

		private const string DEFAULT_TYPE = "Program";
		private const string DEFAULT_METHOD = "Main";

		#endregion

		#region Variables

		private ILog _log;
		private Compiler _compiler;

		#endregion

		#region Constructors

		public Executor(string filename)
		{
			Require.Variable(() => filename).IsNotNullOrEmpty();

			_log = LogManager.GetLogger(typeof(Executor));

			_compiler = Compiler.Compile(new CompilerParameters()
				{
					GenerateExecutable = false,
					GenerateInMemory = true,
					IncludeDebugInformation = true,
					TreatWarningsAsErrors = false,
				}, filename);
		}

		#endregion

		#region Properties

		public string Filename
		{
			get
			{
				return _compiler.Filename;
			}
		}

		public string Script
		{
			get
			{
				return _compiler.Script;
			}
		}

		private CompilerParameters Parameters { get; set; }

		#endregion

		#region Methods

		/// <summary>
		/// Attempt to execute the Main method in the Program class.
		/// </summary>
		public object Execute(params string[] args)
		{
			_log.Info("Executing script.");
			Require.Variable(() => args).IsNotNull();
			return Execute(DEFAULT_TYPE, DEFAULT_METHOD, args);
		}

		/// <summary>
		/// Attempt to execute ClassName.MethodName.
		/// </summary>
		/// <param name="fullyQualifiedName">ClassName.MethodName</param>
		/// <param name="args"></param>
		public object Execute(string fullyQualifiedName, params string[] args)
		{
			Require.Variable(() => fullyQualifiedName).IsNotNullOrEmpty();
			Require.Variable(() => args).IsNotNull();

			int methodSplitIndex = fullyQualifiedName.LastIndexOf('.');
			var names = new string[] { fullyQualifiedName.Substring(0, methodSplitIndex), fullyQualifiedName.Substring(methodSplitIndex + 1, fullyQualifiedName.Length - methodSplitIndex - 1) };
			Require.Variable(() => names).IsOfLength(2);

			return Execute(names[0], names[1], args);
		}

		public object Execute(string typeName, string methodName, params string[] args)
		{
			Require.Variable(() => typeName).IsNotNullOrEmpty();
			Require.Variable(() => methodName).IsNotNullOrEmpty();
			Require.Variable(() => args).IsNotNull();

			if (_compiler.Assembly == null)
			{
				_log.Fatal("Unable to compile script.");
				return null;
			}

			var type = _compiler.Assembly.GetType(typeName);
			if (type == null)
			{
				_log.Fatal("Type does not exist.");
				return null;
			}

			var method = type.GetMethod(methodName);
			if (method == null)
			{
				_log.Fatal("Method does not exist.");
				return null;
			}

			var parameters = method.GetParameters();
			object[] scriptArgs = new object[args.Length];
			if (parameters.Length != scriptArgs.Length)
			{
				_log.FatalFormat("Invalid parameter count, expected: {0}", parameters.Length);
				return null;
			}

			for (int index = 0; index < parameters.Length; index++)
			{
				try
				{
					scriptArgs[index] = Convert.ChangeType(args[index], parameters[index].ParameterType);
				}
				catch (Exception)
				{
					_log.FatalFormat("Parameter {0} is of the wrong type, expected: {1}", index, parameters[index].ParameterType.Name);
					return null;
				}
			}

			try
			{
				return method.Invoke(null, scriptArgs);
			}
			catch (Exception ex)
			{
				_log.Fatal(ex.ToString());
				return null;
			}
		}

		#endregion
	}
}